// ast + 虚拟 dom
// https://developer.aliyun.com/article/1495228
//  html 转换用到的几大件
export interface IYedongHtmlVisitorOptions {
  renderLatex?: (text: string, block?: boolean) => HTMLElement,
  renderHighlight?: (code: string, lang: string) => HTMLElement,
  renderMermaid?: (text: string) => Promise<string>
  renderPlantuml?: (text: string) => Promise<string>
}

export class YedongHtmlVisitor {

  opts: IYedongHtmlVisitorOptions
  oldNodes: any[]
  rootEl: HTMLElement

  constructor(rootEl: HTMLElement, opts: IYedongHtmlVisitorOptions) {
    this.opts = opts;
    this.oldNodes = []
    this.rootEl = rootEl
  }

  removeAllChildNodes(parent: HTMLElement) {
    while (parent.firstChild) {
      parent.removeChild(parent.firstChild);
    }

    // parent.children.forEach(child => {
    //   parent.removeChild(child)
    // })
  }

  async render(nodes: any[]): Promise<void> {
    // console.log(`oldNodes.length = ${this.oldNodes.length}, nodes.length = ${nodes.length}`)
    // console.log(`render oldNode = ${JSON.stringify(this.oldNodes)}`)
    // console.log(`render newNode = ${JSON.stringify(nodes)}`)
    if (!this.oldNodes.length) {
      await this.init(nodes)
    } else {
      // for (let index = 0; index < nodes.length; index++) {
      //   await this.update(this.oldNodes[index], nodes[index])
      // }
      await this.updateChildren(this.rootEl, this.oldNodes, nodes)
    }
    this.oldNodes = nodes
  }

  async init(nodes: any[]): Promise<void> {
    if (!nodes?.length) return;
    for (let index = 0; index < nodes.length; index++) {
      await this.visitNode(this.rootEl, nodes[index])
    }
    // this.rootEl.appendChild(this.rootEl)
    // this.rootEl.replaceChildren(this.rootEl)
  }

  async update(oldNode: any, newNode: any) {
    if (oldNode === newNode) return
    const el = newNode.el = oldNode.el
    const parent = newNode.parent = oldNode.parent as HTMLElement
    const tagName = newNode.tag
    // 更新 attrs
    const attrEqual = this.objectsEqual(oldNode.attrs, newNode.attrs)
    if (!attrEqual) {
      let keys: string[] = []
      if (oldNode.attrs) keys = keys.concat(Object.keys(oldNode.attrs))
      if (newNode.attrs) keys = keys.concat(Object.keys(newNode.attrs))
      const latexNotChanged = (tagName === 'f' || tagName === 'bf') && (oldNode.props.text === newNode.props.text)
      for (const key of keys) {
        const newVal = newNode.attrs?.[key]
        if (key === 'class' && latexNotChanged) {
          const latexClass = tagName === 'f' ? 'katex' : 'display-katex'
          if (!newVal) {
            el.className = latexClass
          } else if (newVal !== oldNode.attrs?.[key]) {
            el.className = latexClass + ' ' + newVal
          }
        } else {
          if (!newVal) {
            el.removeAttribute(key)
          } else if (newVal !== oldNode.attrs?.[key]) {
            if (key === 'class') {
              el.className = newVal
            } else {
              el.setAttribute(key, newVal)
            }
          }
        }
      }
    }
    // console.log(`update tagName = ${tagName}, oldNode.props.text = ${oldNode.props?.text}, newNode.props.text = ${newNode.props?.text}`)
    // console.log(`update oldNode = ${JSON.stringify(oldNode)}`)
    // console.log(`update newNode = ${JSON.stringify(newNode)}`)
    if (tagName === 'text') {
      if (oldNode.props.text !== newNode.props.text) {
        el.nodeValue = newNode.props.text
      }
    } else if (tagName === 'f' || tagName === 'fb' || tagName === 'code' || tagName === 'script') {
      // 这几个节点比较特殊，根据 props.text 决定内容，而且不会有子节点
      if (oldNode.props.text !== newNode.props.text) {
        // 替换节点
        await this.replaceVisitNode(parent, newNode, oldNode.el)
      }
    } else {
      if (tagName === 'img' && oldNode.attrs.caption !== newNode.attrs.caption) {
        await this.replaceVisitNode(parent, newNode, oldNode.el)
      }
      await this.updateChildren(el, oldNode.children, newNode.children)
    }
  }

  async updateChildren(parent: HTMLElement, oldCh: any[], newCh: any[]) {
    if ((!oldCh && !newCh) || (oldCh.length === 0 && newCh.length === 0)) {
      return
    }
    if (oldCh.length > 0 && newCh.length > 0) {
      // 比对
      await this.pathChildren(parent, oldCh, newCh)
    } else if (newCh.length > 0) {
      //  增添 children
      await this.visitNodes(parent, newCh)
    } else {
      // 删除 children
      this.removeAllChildNodes(parent)
    }
  }

  objectsEqual(obj1: any, obj2: any): boolean {
    // 检查两个参数是否是对象
    if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
      return obj1 === obj2;
    }

    // 检查两个对象是否为空
    if (obj1 === null || obj2 === null) {
      return obj1 === obj2;
    }

    // 比较属性数量
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if (obj1Keys.length !== obj2Keys.length) {
      return false;
    }

    // 比较属性值
    for (const key of obj1Keys) {
      const val1 = obj1[key];
      const val2 = obj2[key];
      // 递归比较属性值
      const areObjects = typeof val1 === 'object' && typeof val2 === 'object';
      if (areObjects) {
        if (!this.objectsEqual(val1, val2)) return false
      } else {
        if (val1 !== val2) return false
      }
    }

    // 所有属性都相同
    return true;
  }

  isSameNode(n1: any, n2: any): boolean {
    return n1.tag === n2.tag /*&& this.objectsEqual(n1.props, n2.props)*/
  }

  async pathChildren(parent: HTMLElement, oldCh: any[], newCh: any[]) {
    let oldStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newStartIdx = 0
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (!oldStartVnode) {
        oldStartVnode = oldCh[++oldStartIdx]
      } else if (!oldEndVnode) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (this.isSameNode(oldStartVnode, newStartVnode)) {
        await this.update(oldStartVnode, newStartVnode)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (this.isSameNode(oldEndVnode, newEndVnode)) {
        await this.update(oldEndVnode, newEndVnode)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (this.isSameNode(oldStartVnode, newEndVnode)) { // 删除元素时
        await this.update(oldStartVnode, newEndVnode)
        // move right
        parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (this.isSameNode(oldEndVnode, newStartVnode)) { // 插入元素时
        // moved left
        await this.update(oldEndVnode, newStartVnode)
        parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 这里在源码中会有一个构建key map的操作，但是我们这里没有唯一的key，有index来做key可能会有误判，所以索性就不做这段处理了
        // 从[oldStartIdx, oldEndIdx] 找到 newStartVnode 相同的元素
        let idxInOld = oldStartIdx
        while (idxInOld <= oldEndIdx) {
          if (this.isSameNode(newStartVnode, oldCh[idxInOld])) {
            break
          }
          idxInOld++
        }
        if (idxInOld > oldEndIdx) {
          // newStartVnode 是新节点，插入到 oldStartVnode.el 前面
          await this.insertBeforeVisitNode(parent, newStartVnode, oldStartVnode.el)
        } else {
          const vnodeToMove = oldCh[idxInOld]
          await this.update(vnodeToMove, newStartVnode)
          oldCh[idxInOld] = undefined
          parent.insertBefore(vnodeToMove.el, oldStartVnode.el)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      const refEl = !newCh[newEndIdx + 1] ? null : newCh[newEndIdx + 1].el
      for (let i = newStartIdx; i <= newEndIdx; i++) {
        await this.insertBeforeVisitNode(parent, newCh[i], refEl)
      }
    } else if (newStartIdx > newEndIdx) {
      for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        if (oldCh[i]) { // 移动过程中 oldCh[i] = undefined
          parent.removeChild(oldCh[i].el)
        }
      }
    }
  }

  async visitNodes(parent: HTMLElement, nodes: any[]): Promise<void> {
    if (!nodes?.length) return;
    for (let index = 0; index < nodes.length; index++) {
      await this.visitNode(parent, nodes[index])
    }
  }

  async _visitNode(parent: HTMLElement, node: any): Promise<HTMLElement | Text | null | undefined> {
    const tagName = node.tag
    let el: HTMLElement | Text | null | undefined
    switch (tagName) {
      case 'text':
        el = this.visitText(node)
        break
      // case 'img':
      //   el = this.visitImg(node)
      //   break
      case 'latex':
      case 'block-latex':
      case 'f':
      case 'fb':
        el = this.visitLatex(node)
        break
      case 'code':
        el = this.visitCode(node)
        break
      case 'script':
        el = await this.visitScript(node)
        break
      case 'mermaid':
        el = await this.visitMermaid(node)
        break
      case 'plantuml':
        el = await this.visitPlantuml(node)
        break
      default:
        el = this.visitTag(node)
        break
    }

    if (el) {
      if ((el.nodeType !== Node.TEXT_NODE) && node.children?.length) {
        // @ts-ignore
        await this.visitNodes(el, node.children)
      }
      // 用于对比更新部分
      node.el = el
      node.parent = parent
    }
    return el
  }

  async visitNode(parent: HTMLElement, node: any) {
    const el = await this._visitNode(parent, node)
    if (el) {
      parent.appendChild(el)
    }
  }

  async insertBeforeVisitNode(parent: HTMLElement, node: any, beforeNode: HTMLElement) {
    const el = await this._visitNode(parent, node)
    if (el) {
      parent.insertBefore(el, beforeNode)
    }
  }

  async replaceVisitNode(parent: HTMLElement, node: any, replaceNode: HTMLElement) {
    const el = await this._visitNode(parent, node)
    if (el) {
      if (replaceNode) {
        parent.replaceChild(el, replaceNode)
      } else {
        parent.appendChild(el)
      }
    }
    // else {
    //   parent.removeChild(replaceNode)
    // }
  }

  empty(inline = true, str?: string): HTMLElement {
    const el = document.createElement(inline ? 'span' : 'p')
    if (str) {
      el.nodeValue = str
    }
    return el;
  }

  visitTag(node: any): HTMLElement | null | undefined {
    const tagName = node.tag
    const attrs = node.attrs;
    const el = document.createElement(tagName)
    if (attrs) {
      for (const attr in attrs) {
        el.setAttribute(attr, attrs[attr])
      }
    }
    return el
  }

  visitImg(node: any): HTMLElement | null | undefined {
    const attrs = node.attrs;
    const el = document.createElement("figure")
    // img
    const img = document.createElement("img")
    if (attrs) {
      for (const attr in attrs) {
        if (attr !== 'caption') {
          img.setAttribute(attr, attrs[attr])
        }
      }
    }
    el.appendChild(img)
    const caption = attrs?.caption ?? ''
    if (caption) {
      const figcaption = document.createElement("figcaption")
      figcaption.appendChild(document.createTextNode(caption))
      el.appendChild(figcaption)
    }
    return el
  }

  visitText(node: any): Text {
    return document.createTextNode(node.props.text)
  }

  async visitScript(node: any): Promise<HTMLElement | null | undefined> {
    if (node.attrs.lang === 'mermaid') {
      return await this.visitMermaid(node)
    } else if (node.attrs.lang === 'plantuml') {
      return await this.visitPlantuml(node)
    } else {
      return this.visitCode(node)
    }
  }

  visitCode(node: any): HTMLElement {
    if (!this.opts.renderHighlight) {
      return this.empty(false, node.props.text)
    }
    return this.opts.renderHighlight(node.props.text, node.attrs.lang)
  }

  visitLatex(node: any): HTMLElement | null {
    if (!this.opts.renderLatex) {
      return this.empty(node.props.inline, node.props.text)
    }
    const el = this.opts.renderLatex(node.props.text, !node.props.inline)
    const attrs = node.attrs;
    if (attrs) {
      for (const attr in attrs) {
        if (attr === 'class') {
          el.classList.add(...attrs[attr].split(' '))
        } else {
          el.setAttribute(attr, attrs[attr])
        }
      }
    }
    el.setAttribute('data-formula', node.props.text)
    return el
  }

  async visitMermaid(node: any): Promise<HTMLElement | null | undefined> {
    if (!this.opts?.renderMermaid) {
      return this.empty(false, node.props.text)
    }
    const svg = await this.opts.renderMermaid(node.props.text)
    const div = document.createElement('div');
    div.classList.add('align-center')
    // div.style.textAlign = 'center'
    div.innerHTML = svg;
    return div;
    // https://www.cnblogs.com/qingsongge/p/17560828.html
    // https://www.jianshu.com/p/a42d4e72ae29
    // const template = document.createElement('template');
    // template.innerHTML = svg;
    // return template.content.firstElementChild as HTMLElement;

    // const range = document.createRange();
    // const fragment = range.createContextualFragment(svg);
    // return fragment;

    // 设置 DocumentFragment 中所有元素的属性
    // frag.firstElementChild 根节点
    // var allParagraphs = frag.querySelectorAll('p');
    // allParagraphs.forEach(function(p) {
    //   p.setAttribute('class', 'paragraph');
    // });
  }

  async visitPlantuml(node: any): Promise<HTMLElement | null | undefined> {
    if (!this.opts?.renderPlantuml) {
      return this.empty(false, node.props.text)
    }
    const svg = await this.opts.renderPlantuml(node.props.text)
    const div = document.createElement('div');
    div.classList.add('align-center')
    // div.style.textAlign = 'center'
    div.innerHTML = svg;
    return div;

    // const parser = new DOMParser();
    // const doc = parser.parseFromString(svg, "text/html"); // 第2个参数为mimeType，可以为 text/html, text/xml 等等这些
    // return doc.body;
  }
}
